﻿using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.Xna.Framework;

namespace SpriteAtlasRuntime
{
    public static class Animator
    {
        private static uint mNextId = 0;
        private static Dictionary<uint, AnimationScript> mScripts = new Dictionary<uint, AnimationScript>();
        private static Dictionary<ISpriteComponent, uint> mAnimatedComponentScriptIndexes = new Dictionary<ISpriteComponent, uint>();
        private static Dictionary<IModelComponent, uint> mModelComponentScriptIndexes = new Dictionary<IModelComponent, uint>();

        internal static void RegisterScript(AnimationScript script)
        {
            uint index = mNextId++;
            mScripts.Add(index, script);

            if (script.SpriteTarget != null)
            {
                mAnimatedComponentScriptIndexes.Add(script.SpriteTarget, index);
            }

            if (script.ModelTarget != null)
            {
                mModelComponentScriptIndexes.Add(script.ModelTarget, index);
            }
        }

        public static void RemoveScript(ISpriteComponent animatedComponent)
        {
            uint index = mAnimatedComponentScriptIndexes[animatedComponent];
            mAnimatedComponentScriptIndexes.Remove(animatedComponent);

            if (mScripts[index].ModelTarget != null)
            {
                mModelComponentScriptIndexes.Remove(mScripts[index].ModelTarget);
            }

            mScripts.Remove(index);
        }

        public static void RemoveScript(IModelComponent modelComponent)
        {
            uint index = mModelComponentScriptIndexes[modelComponent];
            mModelComponentScriptIndexes.Remove(modelComponent);

            if (mScripts[index].SpriteTarget != null)
            {
                mAnimatedComponentScriptIndexes.Remove(mScripts[index].SpriteTarget);
            }

            mScripts.Remove(index);
        }

        public static void Update(float ms)
        {
            List<AnimationScript> deadScripts = new List<AnimationScript>();
            foreach (var key in mScripts.Keys)
            {
                AnimationScript script = mScripts[key];
                RunScript(ms, script);

                if (script.CurCommand == ScriptCommands.End)
                {
                    deadScripts.Add(script);
                }
            }

            foreach (var script in deadScripts)
            {
                if (script.ModelTarget != null)
                {
                    RemoveScript(script.ModelTarget);
                }
                else
                {
                    Debug.Assert(script.SpriteTarget != null);
                    RemoveScript(script.SpriteTarget);
                }
            }
        }

        #region Command interpreters
        private static void RunScript(float ms, AnimationScript script)
        {
            Debug.Assert(script.CurCommand != ScriptCommands.End);

            while (ms > 0 && script.TimeRemaining <= ms)
            {
                ms -= script.TimeRemaining;
                script.TimeRemaining = 0;

                switch (script.CurCommand)
                {
                    // hidden command: skip bytes
                    case (ScriptCommands)(ScriptCommands.End - 1):
                        ParseSkip(script);
                        break;

                    case ScriptCommands.End:
                        ParseEnd(script);
                        return;

                    case ScriptCommands.Set:
                        ParseSet(script);
                        break;

                    case ScriptCommands.If:
                        ParseIf(script);
                        break;

                    case ScriptCommands.Label:
                        ParseLabel(script);
                        break;

                    case ScriptCommands.Goto:
                        ParseGoto(script);
                        break;

                    case ScriptCommands.Jump:
                        ParseJump(script);
                        break;

                    default:
                        Debug.Assert(false);
                        return;
                }

                script.ReadNextCommand();
            }

            // if we exited the loop because there wasn't enough time left in the update,
            // save the rest of this event for the next one
            if (ms != 0)
            {
                Debug.Assert(script.TimeRemaining > ms);
                script.TimeRemaining -= ms;
            }
        }

        private static void ParseSkip(AnimationScript script)
        {
            int skipBytes = script.ReadNextInt();
            script.SkipBytes(skipBytes);
        }

        private static void ParseEnd(AnimationScript script)
        {
        }

        private static void ParseSet(AnimationScript script)
        {
            switch (script.ReadNextTarget())
            {
                case ScriptTargets.Speed:
                    {
                        float speed = script.ReadNextFloat();

                        Debug.Assert(script.ModelTarget != null, "Script updates model, but does not have a model target.");
                        script.ModelTarget.Speed = speed;
                    }
                    break;

                case ScriptTargets.Acceleration:
                    {
                        float a = script.ReadNextFloat();

                        Debug.Assert(script.ModelTarget != null, "Script updates model, but does not have a model target.");
                        script.ModelTarget.Acceleration = a;
                    }
                    break;

                case ScriptTargets.MaxSpeed:
                    {
                        float speed = script.ReadNextFloat();

                        Debug.Assert(script.ModelTarget != null, "Script updates model, but does not have a model target.");
                        script.ModelTarget.MaxSpeed = speed;
                    }
                    break;

                case ScriptTargets.PolarVelocity:
                    {
                        float v = script.ReadNextFloat();
                        float r = script.ReadNextFloat();

                        Debug.Assert(script.ModelTarget != null, "Script updates model, but does not have a model target.");
                        script.ModelTarget.SetVelocity(v, r);
                    }
                    break;

                case ScriptTargets.PolarAcceleration:
                    {
                        float a = script.ReadNextFloat();
                        float r = script.ReadNextFloat();

                        Debug.Assert(script.ModelTarget != null, "Script updates model, but does not have a model target.");
                        script.ModelTarget.SetAcceleration(a, r);
                    }
                    break;

                case ScriptTargets.Velocity_X:
                    {
                        float vx = script.ReadNextFloat();

                        Debug.Assert(script.ModelTarget != null, "Script updates model, but does not have a model target.");
                        script.ModelTarget.SetVelocity_X(vx);
                    }
                    break;

                case ScriptTargets.Velocity_Y:
                    {
                        float vy = script.ReadNextFloat();

                        Debug.Assert(script.ModelTarget != null, "Script updates model, but does not have a model target.");
                        script.ModelTarget.SetVelocity_Y(vy);
                    }
                    break;

                case ScriptTargets.Acceleration_X:
                    {
                        float ax = script.ReadNextFloat();

                        Debug.Assert(script.ModelTarget != null, "Script updates model, but does not have a model target.");
                        script.ModelTarget.SetAcceleration_X(ax);
                    }
                    break;

                case ScriptTargets.Acceleration_Y:
                    {
                        float ay = script.ReadNextFloat();

                        Debug.Assert(script.ModelTarget != null, "Script updates model, but does not have a model target.");
                        script.ModelTarget.SetAcceleration_Y(ay);
                    }
                    break;

                case ScriptTargets.BoundingRegions:
                    {
                        int count = script.ReadNextInt();

                        List<float[]> regions = new List<float[]>();

                        while (count-- > 0)
                        {
                            switch ((BoundingRegionTypes)script.ReadNextInt())
                            {
                                case BoundingRegionTypes.Circle:
                                    {
                                        float x = script.ReadNextFloat();
                                        float y = script.ReadNextFloat();
                                        float r = script.ReadNextFloat();

                                        regions.Add(new float[] { x, y, r });
                                    }
                                    break;

                                case BoundingRegionTypes.Rectangle:
                                    {
                                        float l = script.ReadNextFloat();
                                        float t = script.ReadNextFloat();
                                        float w = script.ReadNextFloat();
                                        float h = script.ReadNextFloat();

                                        regions.Add(new float[] { l, t, w, h });
                                    }
                                    break;
                            }
                        }

                        Debug.Assert(script.ModelTarget != null, "Script updates model, but does not have a model target.");
                        script.ModelTarget.SetBoundingRegions(regions);
                    }
                    break;

                case ScriptTargets.Flag:
                    {
                        int flag = script.ReadNextInt();

                        Debug.Assert(script.ModelTarget != null, "Script updates model, but does not have a model target.");
                        script.ModelTarget.SetFlag(flag);
                    }
                    break;

                case ScriptTargets.KeyValuePair:
                    {
                        string key = script.ReadNextString();
                        float value = script.ReadNextFloat();

                        Debug.Assert(script.ModelTarget != null, "Script updates model, but does not have a model target.");
                        script.ModelTarget.SetKeyValuePair(key, value);
                    }
                    break;

                case ScriptTargets.TextureRect:
                    {
                        int x = script.ReadNextInt();
                        int y = script.ReadNextInt();
                        int w = script.ReadNextInt();
                        int h = script.ReadNextInt();

                        Debug.Assert(script.SpriteTarget != null, "Script updates animation, but does not have an animation target.");
                        script.SpriteTarget.Rectangle = new Rectangle(x, y, w, h);
                    }
                    break;

                case ScriptTargets.TextureHotspot:
                    {
                        float x = script.ReadNextFloat();
                        float y = script.ReadNextFloat();

                        Debug.Assert(script.SpriteTarget != null, "Script updates animation, but does not have an animation target.");
                        script.SpriteTarget.Hotspot = new Vector2(x, y);
                    }
                    break;

                case ScriptTargets.TextureFlip:
                    {
                        int key = script.ReadNextInt();

                        if ((key & 0x1) == 0x1)
                        {
                            bool flipHorz = script.ReadNextBool();

                            Debug.Assert(script.SpriteTarget != null, "Script updates animation, but does not have an animation target.");
                            script.SpriteTarget.FlipHorizontal = flipHorz;
                        }

                        if ((key & 0x2) == 0x2)
                        {
                            bool flipVert = script.ReadNextBool();

                            Debug.Assert(script.SpriteTarget != null, "Script updates animation, but does not have an animation target.");
                            script.SpriteTarget.FlipVertical = flipVert;
                        }
                    }
                    break;

                case ScriptTargets.SpriteOpacity:
                    {
                        float opacity = script.ReadNextFloat();

                        Debug.Assert(script.SpriteTarget != null, "Script updates animation, but does not have an animation target.");
                        script.SpriteTarget.Opacity = opacity;
                    }
                    break;
            }

            // the last value is the amount of time the animator should apply towards this command
            script.ReadThisCommandTime();
        }

        private static void ParseIf(AnimationScript script)
        {
            CompareTargets compareTarget = (CompareTargets)script.ReadNextInt();
            CompareTypes compareType = (CompareTypes)script.ReadNextInt();
            float compareValue = script.ReadNextFloat();
            int thenBlockBytes = script.ReadNextInt();

            Debug.Assert(script.ModelTarget != null, "Script accesses model, but does not have a model target.");

            float actualValue = 0.0f;
            if (compareTarget == CompareTargets.Speed)
            {
                actualValue = script.ModelTarget.Speed;
            }
            else if (compareTarget == CompareTargets.Position_X)
            {
                actualValue = script.ModelTarget.Position.X;
            }
            else if (compareTarget == CompareTargets.Position_Y)
            {
                actualValue = script.ModelTarget.Position.Y;
            }
            else
            {
                Debug.Assert(false, "Script has an invalid/unknown CompareTargets type.");
            }

            bool compareResult = false;
            switch (compareType)
            {
                case CompareTypes.LessThan:
                    compareResult = actualValue < compareValue;
                    break;

                case CompareTypes.LessThanOrEqual:
                    compareResult = actualValue <= compareValue;
                    break;

                case CompareTypes.Equal:
                    compareResult = actualValue == compareValue;
                    break;

                case CompareTypes.NotEqual:
                    compareResult = actualValue != compareValue;
                    break;

                case CompareTypes.GreaterThanOrEqual:
                    compareResult = actualValue >= compareValue;
                    break;

                case CompareTypes.GreaterThan:
                    compareResult = actualValue > compareValue;
                    break;

                default:
                    Debug.Assert(false, "Script has an invalid/unknown CompareTypes type.");
                    break;
            }

            if (compareResult)
            {
                // the next command is the hidden "skip" command
                int nextCommand = script.ReadNextInt();
                Debug.Assert(nextCommand == (int)ScriptCommands.End - 1);

                // the skip command has a number of bytes to skip
                script.ReadNextInt();
            }
        }

        private static void ParseLabel(AnimationScript script)
        {
            string labelName = script.ReadNextString();
            script.SaveLabel(labelName);
        }

        private static void ParseGoto(AnimationScript script)
        {
            string labelName = script.ReadNextString();
            script.GotoLabel(labelName);
        }

        private static void ParseJump(AnimationScript script)
        {
            int scriptId = script.ReadNextInt();
            script.SetScript(scriptId);
        }
        #endregion
    }
}
